package com.nulldev.util.internal.backport.httpclient_rw.httpclient;

import java.io.IOException;
import java.io.InputStream;
import java.net.CookieHandler;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.Arrays.maps.EmptyMap;
import com.nulldev.util.internal.backport.httpclient_rw.HttpClient;
import com.nulldev.util.internal.backport.httpclient_rw.HttpClient.Version;
import com.nulldev.util.internal.backport.httpclient_rw.HttpRequest.BodyPublisher;
import com.nulldev.util.internal.backport.httpclient_rw.HttpRequest.BodyPublishers;
import com.nulldev.util.internal.backport.httpclient_rw.HttpRequest.Builder;
import com.nulldev.util.internal.backport.httpclient_rw.HttpResponse.BodyHandler;
import com.nulldev.util.internal.backport.httpclient_rw.HttpResponse.BodyHandlers;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.throttling.BandwidthThrottler;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.HttpClient.HttpRequest;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpClient.ResponseFormat;
import com.nulldev.util.web.HttpClient.contentGen.GeneratableContent;
import com.nulldev.util.web.HttpClient.util.HttpClientUtil;
import com.nulldev.util.web.HttpClient.util.HttpStreamUtil;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;

public abstract class BackportedHttpRequest<T> implements HttpRequest<T> {
	protected final static Logger logger = LoggerFactory.getLoggerD(Arguments.hasArgument("--HttpClient.backportLogging"));

	private static final boolean ASYNC_USE_PROVIDED_FUNC = Arguments.notArguments("--HttpClient.useProvidedAsync");
	protected final static HttpClient client = HttpClient.newBuilder()
			/* */
			.executor(com.nulldev.util.web.HttpClient.HttpClient.gld.executor())
			/* */
			.version((HttpClientUtil.hasALPN() ? Version.HTTP_2 : Version.HTTP_1_1)).cookieHandler(new CookieHandler() {

				public final Map<String, List<String>> COOKIE_LIST = new EmptyMap<String, List<String>>();

				@Override
				public void put(URI uri, Map<String, List<String>> responseHeaders) throws IOException {
				}

				@Override
				public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) throws IOException {
					return COOKIE_LIST;
				}
			})
			/* */
			.build();
	private Builder connection;

	public BackportedHttpRequest() {
		this.connection = com.nulldev.util.internal.backport.httpclient_rw.HttpRequest.newBuilder();
		this.forceHeader("User-Agent", com.nulldev.util.web.HttpClient.HttpClient.USER_AGENT);
	}

	@Override
	public HttpRequest<T> url(final String url) throws Exception {
		this.connection = this.connection.uri(new URI(url));
		return this;
	}

	@Override
	public HttpRequest<T> url(final URL url) throws Exception {
		this.connection = this.connection.uri(url.toURI());
		return this;
	}

	@Override
	public HttpRequest<T> header(final String key, final String value) {
		if (key == null || value == null || key.isEmpty() || value.isEmpty() || key.toLowerCase().equals("content-length")) {
			return this;
		}
		this.connection = this.connection.header(key, value);
		return this;
	}

	@Override
	public HttpRequest<T> forceHeader(final String key, final String value) {
		if (key == null || value == null || key.isEmpty() || value.isEmpty() || key.toLowerCase().equals("content-length")) {
			return this;
		}
		this.connection = this.connection.setHeader(key, value);
		return this;
	}

	@Override
	public HttpRequest<T> header(final Entry entry) {
		if (entry == null || !entry.isValid() || entry.getKey().toLowerCase().equals("content-length")) {
			return this;
		}
		this.connection = this.connection.header(entry.getKey(), entry.getValue());
		return this;
	}

	@Override
	public HttpRequest<T> headers(final Entry[] entries) {
		if (entries == null || entries.length == 0) {
			return this;
		}
		for (final Entry entry : entries) {
			if (entry == null || !entry.isValid() || entry.getKey().toLowerCase().equals("content-length")) {
				continue;
			}
			this.connection = this.connection.header(entry.getKey(), entry.getValue());
		}
		return this;
	}

	@Override
	public HttpRequest<T> timeout(final Duration duration) {
		if (duration == null)
			return this;
		this.connection = this.connection.timeout(duration);
		return this;
	}

	@SuppressWarnings("unchecked")
	@Override
	public HttpResponse<T> create(final HttpMethod method, final Object body) throws Exception {
		logger.debug("create(): method=" + method + ",body=" + body);
		if (body == null || method == HttpMethod.GET) {
			logger.debug("create(): GET");
			this.connection = this.connection.GET();
		} else {
			BodyPublisher bp = BodyPublishers.noBody();
			if (body instanceof String) {
				bp = BodyPublishers.ofString((String) body);
			} else if (body instanceof InputStream) {
				bp = BodyPublishers.ofInputStream(new Supplier<InputStream>() {

					@Override
					public InputStream get() {
						return (InputStream) body;
					}
				});
			} else if (body instanceof byte[]) {
				bp = BodyPublishers.ofByteArray((byte[]) body);
			} else if (body instanceof GeneratableContent) {
				bp = BodyPublishers.ofString(((GeneratableContent) body).makeAndClear());
			} else {
				throw new UnsupportedOperationException("Unable to create BodyPublisher from: " + body);
			}
			logger.debug("create(): bp=" + bp);
			if (method == HttpMethod.OPTIONS) {
				throw new UnsupportedOperationException("OPTIONS is not supported.");
			} else if (method == HttpMethod.PUT) {
				this.connection = this.connection.PUT(bp);
			} else if (method == HttpMethod.DELETE) {
				this.connection = this.connection.DELETE();
			} else if (method == HttpMethod.POST) {
				logger.debug("create(): pre-POST");
				this.connection = this.connection.POST(bp);
				logger.debug("create(): POST");
			} else {
				throw new UnsupportedOperationException("Unknown method: " + method);
			}
		}
		final ResponseFormat format = this.getFormat();
		if (format.allowCompression) {
			this.header("Accept-Encoding", HttpStreamUtil.getSupportedCompressionString());
		}
		if (body instanceof GeneratableContent) {
			this.header("Content-Type", ((GeneratableContent) body).getContentType());
		}
		if (this.spoofChromium.get()) {
			this.header("Sec-Ch-Ua", URLUtils.makeChromiumUAHeader());
			this.header("Sec-Ch-Ua-Mobile", "?0");
			this.header("Sec-Ch-Ua-Platform", "\"Windows\"");
			this.header("Sec-Fetch-User", "?1");
		}

		final com.nulldev.util.internal.backport.httpclient_rw.HttpRequest r = this.connection.build();
		BodyHandler<?> o = null;
		if (format == ResponseFormat.STRING) {
			o = BodyHandlers.ofString();
		} else if (format == ResponseFormat.BYTES) {
			o = BodyHandlers.ofByteArray();
		} else {
			o = BodyHandlers.ofInputStream();
		}
		if (bufferSize.get() > 0 && o != null) {
			o = BodyHandlers.buffering(o, this.bufferSize.get());
		}
		logger.debug("create(): o=" + o);

		final com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T> x = (com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T>) client
				.send(r, o);
		logger.debug("create(): sent!");
		if (body != null && body instanceof InputStream)
			IOUtils.closeQuietly((InputStream) body);
		return new BackportedHttpResponse<T>((com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T>) x, format, this.throttler,
				this.feature_flag_os_monitor.get());
	}

	@SuppressWarnings("unchecked")
	@Override
	public Executable<HttpResponse<T>> createTask(final HttpMethod method, final Object body) throws Exception {
		final ControllableFutureTask<HttpResponse<T>> rr = new ControllableFutureTask<HttpResponse<T>>();
		try {
			logger.debug("createTask(): method=" + method + ",body=" + body);
			if (body == null || method == HttpMethod.GET) {
				logger.debug("createTask(): GET");
				BackportedHttpRequest.this.connection = BackportedHttpRequest.this.connection.GET();
			} else {
				BodyPublisher bp = BodyPublishers.noBody();
				if (body instanceof String) {
					bp = BodyPublishers.ofString((String) body);
				} else if (body instanceof InputStream) {
					bp = BodyPublishers.ofInputStream(new Supplier<InputStream>() {

						@Override
						public InputStream get() {
							return (InputStream) body;
						}
					});
				} else if (body instanceof byte[]) {
					bp = BodyPublishers.ofByteArray((byte[]) body);
				} else if (body instanceof GeneratableContent) {
					bp = BodyPublishers.ofString(((GeneratableContent) body).makeAndClear());
				} else {
					throw new UnsupportedOperationException("Unable to create BodyPublisher from: " + body);
				}
				logger.debug("createTask(): bp=" + bp);
				if (method == HttpMethod.OPTIONS) {
					throw new UnsupportedOperationException("OPTIONS is not supported.");
				} else if (method == HttpMethod.PUT) {
					BackportedHttpRequest.this.connection = BackportedHttpRequest.this.connection.PUT(bp);
				} else if (method == HttpMethod.DELETE) {
					BackportedHttpRequest.this.connection = BackportedHttpRequest.this.connection.DELETE();
				} else if (method == HttpMethod.POST) {
					logger.debug("createTask(): pre-POST");
					BackportedHttpRequest.this.connection = BackportedHttpRequest.this.connection.POST(bp);
					logger.debug("createTask(): POST");
				} else {
					throw new UnsupportedOperationException("Unknown method: " + method);
				}
			}
			final ResponseFormat format = BackportedHttpRequest.this.getFormat();
			if (format.allowCompression) {
				BackportedHttpRequest.this.header("Accept-Encoding", "gzip, deflate");
			}
			if (body instanceof GeneratableContent) {
				BackportedHttpRequest.this.header("Content-Type", ((GeneratableContent) body).getContentType());
			}
			if (this.spoofChromium.get()) {
				this.header("Sec-Ch-Ua", URLUtils.makeChromiumUAHeader());
				this.header("Sec-Ch-Ua-Mobile", "?0");
				this.header("Sec-Ch-Ua-Platform", "\"Windows\"");
				this.header("Sec-Fetch-User", "?1");
			}
			final com.nulldev.util.internal.backport.httpclient_rw.HttpRequest r = BackportedHttpRequest.this.connection.build();
			BodyHandler<?> o = null;
			if (format == ResponseFormat.STRING) {
				o = BodyHandlers.ofString();
			} else if (format == ResponseFormat.BYTES) {
				o = BodyHandlers.ofByteArray();
			} else {
				o = BodyHandlers.ofInputStream();
			}
			if (bufferSize.get() > 0 && o != null) {
				o = BodyHandlers.buffering(o, this.bufferSize.get());
			}
			logger.debug("createTask(): o=" + o);
			if (ASYNC_USE_PROVIDED_FUNC) {
				client.sendAsync(r, o).whenComplete((result, err) -> {
					if (err != null)
						rr.fail(err);
					else {
						if (body != null && body instanceof InputStream)
							IOUtils.closeQuietly((InputStream) body);
						if (!rr.complete(new BackportedHttpResponse<T>((com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T>) result, format,
								this.throttler, this.feature_flag_os_monitor.get()))) {
							IOUtils.closeQuietly(result);
						}
					}
				});
			} else {
				com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T> x;
				try {
					x = (com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T>) client.send(r, o);
				} catch (IOException | InterruptedException e) {
					throw new RuntimeException(e);
				}
				logger.debug("createTask(): sent!");
				if (body != null && body instanceof InputStream)
					IOUtils.closeQuietly((InputStream) body);
				if (!rr.complete(new BackportedHttpResponse<T>((com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T>) x, format, this.throttler,
						this.feature_flag_os_monitor.get()))) {
					IOUtils.closeQuietly(x);
				}
			}
		} catch (Throwable e) {
			if (body != null && body instanceof InputStream)
				IOUtils.closeQuietly((InputStream) body);
			rr.fail(e);
		}
		return rr;
	}

	@Override
	public Executable<HttpResponse<T>> createTaskAndRun(final HttpMethod method, final Object body) throws Exception {
		return this.createTask(method, body);
	}

	@Override
	public boolean isSupportedMethod(final HttpMethod method) {
		switch (method) {
			case GET:
			case POST:
			case DELETE:
			case PUT:
				return true;
			default:
				return false;
		}
	}

	@Override
	public abstract ResponseFormat getFormat();

	/* TODO: Shouldn't these just be a regular boolean */
	private final AtomicInteger bufferSize = new AtomicInteger(-1);
	private final AtomicBoolean spoofChromium = new AtomicBoolean();
	private final AtomicBoolean feature_flag_os_monitor = new AtomicBoolean();

	@Override
	public HttpRequest<T> enableBuffering(final int bufferSize) {
		if (bufferSize <= 0)
			return this;
		this.bufferSize.set(bufferSize);
		return this;
	}

	@Override
	public boolean isFeatureFlagSupported(final int feature) {
		switch (feature) {
			case FEATURE_FLAG_ENABLE_DOWNSTREAM_THROTTLING:
			case FEATURE_FLAG_SPOOF_CHROMIUM:
			case FEATURE_FLAG_REQUEST_READ_API:
			case FEATURE_FLAG_BACKPORT_ENABLE_INPUT_STREAM_MONITOR:
				return true;
			default:
				return false;
		}
	}

	@Override
	public Object getFeatureFlag(final int feature) {
		switch (feature) {
			case FEATURE_FLAG_ENABLE_DOWNSTREAM_THROTTLING:
				return this.throttler != null ? HTTP_CLIENT_TRUE : HTTP_CLIENT_FALSE;
			case FEATURE_FLAG_SPOOF_CHROMIUM:
				return this.spoofChromium.get();
			case FEATURE_FLAG_BACKPORT_ENABLE_INPUT_STREAM_MONITOR:
				return this.feature_flag_os_monitor.get();
			default:
				return HTTP_CLIENT_NOT_SUPPORTED;
		}
	}

	@Override
	public HttpRequest<T> setFeatureFlag(final int feature, final Object value) {
		if (value == null || !this.isFeatureFlagSupported(feature))
			return this;
		switch (feature) {
			case FEATURE_FLAG_ENABLE_DOWNSTREAM_THROTTLING: {
				if (!(value instanceof BandwidthThrottler))
					break;
				this.throttler = (BandwidthThrottler) value;
				break;
			}
			case FEATURE_FLAG_SPOOF_CHROMIUM:
				this.spoofChromium.set(this._value_asBoolean(value));
				break;
			case FEATURE_FLAG_BACKPORT_ENABLE_INPUT_STREAM_MONITOR:
				this.feature_flag_os_monitor.set(this._value_asBoolean(value));
				break;
			default:
				break;
		}
		return this;
	}

	private BandwidthThrottler throttler;

	/* Read API */

	@Override
	public String url() {
		return this.connection.url();
	}

	@Override
	public Map<String, List<String>> headers() {
		return this.connection.headers();
	}
}
